當大量的請求進來時,快取可以用來降低資料庫的負擔。Moleculer 提供了一個內建的快取解決方案用來快取 Actions 的響應,可以在 ServiceBroker 的選項中設定 cacher 的類型,並在 Actions 中設置 cache: true 以啟用快取。
範例:Actions 快取
const { ServiceBroker } = require("moleculer");
// 建立 Broker
const broker = new ServiceBroker({
    cacher: "Memory"
});
// 建立服務
broker.createService({
    name: "users",
    actions: {
        list: {
            // 在 Action 啟用快取
            cache: true,
            handler(ctx) {
                this.logger.info("Handler called!");
                return [
                    { id: 1, name: "John" },
                    { id: 2, name: "Jane" }
                ];
            }
        }
    }
});
// 測試呼叫 Action
broker.start()
    .then(() => {
        // 第一次呼叫時快取是空的,會執行 Action 處理程序
        return broker.call("users.list").then(
            res => broker.logger.info("Users count:", res.length)
        );
    })
    .then(() => {
        // 第二次呼叫時快取有值,不會執行 Action 處理程序
        return broker.call("users.list").then(
            res => broker.logger.info("Users count from cache:", res.length)
        );
    });
主控台訊息:
[2022-09-17T23:19:05.010Z] INFO  workboxy-32400/BROKER: ✔ ServiceBroker with 2 service(s) started successfully in 15ms.
[2022-09-17T23:19:05.012Z] INFO  workboxy-32400/USERS: Handler called!
[2022-09-17T23:19:05.012Z] INFO  workboxy-32400/BROKER: Users count: 2
[2022-09-17T23:19:05.014Z] INFO  workboxy-32400/BROKER: Users count from cache: 2
你可以發現
Handler called!只出現一次,因為第二次請求的響應是由快取中回傳的。
快取會根據服務名稱、 Action 名稱及參數的 context 來定義鍵值。
格式:
<服務名稱>.<Action 名稱>:<參數或參數的 Hash>
如果你呼叫 posts.list 並給定參數 { limit: 5, offset: 20 } ,這時候快取會根據參數來計算 Hash 值。當下次使用同樣的參數來呼叫同個動作時,快取就會依相同的鍵值來找到該結果。
範例:
posts.list:limit|5|offset|20
由於參數可能夾帶一些與快取無關的屬性,過長的鍵值也可能導致效能問題。因此建議設定好快取的參數屬性,可以有效縮短鍵值並移除無關的屬性。若需要在快取中使用 meta 資訊,鍵的名稱請使用
#前綴。如果參數值未定義,則會以undefined記錄在鍵值中。
範例:
如果參數為 { limit: 10, offset: 30 } 且 meta 為 { user: { id: 123 } } ,將得到快取鍵值 posts.list:10|30|123 。
module.exports = {
    name: "posts",
    actions: {
        list: {
            cache: {
                //由 "limit" 、 "offset" 參數及 "user.id" meta 資訊產生鍵值
                keys: ["limit", "offset", "#user.id"]
            },
            handler(ctx) {
                return this.getList(ctx.params.limit, ctx.params.offset);
            }
        }
    }
};
這個解決方案非常的快,官方推薦在生產環境中使用它。
某些情況下,有效的鍵值可能很長,這可能會導致效能問題。為了避免這種情況,請使用 maxParamsLength 選項來限制鍵值的最大長度。當鍵值長度大於設定的最大長度值時,快取由原始的鍵值計算一個 Hash 值(SHA256) ,然後添加到被裁切的鍵值末端。
maxParamsLength的最小值為 44 (Base64 的 SHA256 長度),若要關閉此功能,請設為 0 或 null 。
範例:未限制長度的情況
cacher.getCacheKey("posts.find", { id: 2, title: "New post", content: "It can be very very looooooooooooooooooong content. So this key will also be too long" });
// 鍵值: 'posts.find:id|2|title|New post|content|It can be very very looooooooooooooooooong content. So this key will also be too long'
範例:限制長度為 60
const broker = new ServiceBroker({
    cacher: {
        type: "Memory",
        options: {
            maxParamsLength: 60
        }
    }
});
cacher.getCacheKey("posts.find", { id: 2, title: "New post", content: "It can be very very looooooooooooooooooong content. So this key will also be too long" });
// 鍵值: 'posts.find:id|2|title|New pL4ozUU24FATnNpDt1B0t1T5KP/T5/Y+JTIznKDspjT0='
快取也允許有條件的跳過快取機制來取得 新的 資料。你可以在呼叫 Action 之前,於 meta 中設定 $cache: false 來關閉此機制。
範例:呼叫 Action 時關閉快取
broker.call("greeter.hello", { name: "Moleculer" }, { meta: { $cache: false }}));
你也可以將快取視為一個選項,客製化一個控制函數來啟動快取。客製化函數能接收 context 實例作為參數,因此可以利用 ctx 來查詢參數或 meta 資訊。
範例:客製化快取條件
greeter.service.js
module.exports = {
    name: "greeter",
    actions: {
        hello: {
            cache: {
                // 由 `noCache` 參數決定是否快取
                enabled: ctx => ctx.params.noCache !== true,
                keys: ["name"]
            },
            handler(ctx) {
                this.logger.debug("Execute handler");
                return `Hello ${ctx.params.name}`;
            }
        }
    }
};
// 使用客製化 `enabled` 函數請求關閉快取
broker.call("greeter.hello", { name: "Moleculer", noCache: true });
你可以在 ServiceBroker 設定快取的存活時間 (Time To Live, TTL) ,也可以在 Action 選項中覆蓋此設定。
const { ServiceBroker } = require("moleculer");
const broker = new ServiceBroker({
    cacher: {
        type: "memory",
        options: {
            ttl: 30 // 30 秒
        }
    }
});
broker.createService({
    name: "posts",
    actions: {
        list: {
            cache: {
                // 存活時間將被覆蓋為 5 秒
                ttl: 5
            },
            handler(ctx) {
                // ...
            }
        }
    }
});
如果要客製化快取鍵值的生成方式,可以在 keygen 設定你自己的客製化函數,並使用 name 、 params 、 meta 與 keys 來建構函數規則。
const broker = new ServiceBroker({
    cacher: {
        type: "memory",
        options: {
            keygen(name, params, meta, keys) {
                // 產生快取鍵值
                // name - action 名稱
                // params - ctx.params
                // meta - ctx.meta
                // keys - action 定義的快取鍵名稱
                return "";
            }
        }
    }
});
快取模組也可以手動使用。只需要呼叫 broker.cacher 的 get 、 set 與 del 方法即可。
// 儲存到快取
broker.cacher.set("mykey.a", { a: 5 });
// 取得快取
const obj = await broker.cacher.get("mykey.a")
// 刪除快取項目
await broker.cacher.del("mykey.a");
// 清除所有 'mykey' 項目
await broker.cacher.clean("mykey.**");
// 清除所有項目
await broker.cacher.clean();
範例:當使用內建 Redis 快取時,可以透過 broker.cacher.client 來使用 ioredis 套件的客戶端 API 。
// 建立 ioredis pipeline
const pipeline = broker.cacher.client.pipeline();
// 設定快取值
pipeline.set('mykey.a', 'myvalue.a');
pipeline.set('mykey.b', 'myvalue.b');
// 執行 pipeline
pipeline.exec();
當你在服務中建立新的資料模型時,你必須清除一些舊的快取模型項目,使下次請求能得到新的資訊,並建立更新後的快取資訊。
範例:在 Action 內清除快取
{
    name: "users",
    actions: {
        create(ctx) {
            // 建立新的使用者實體
            const user = new User(ctx.params);
            // 清除所有快取項目
            this.broker.cacher.clean();
            // 清除所有 `users.` 開頭的快取項目
            this.broker.cacher.clean("users.**");
            // 清除多種快取項目
            this.broker.cacher.clean([ "users.**", "posts.**" ]);
            // 刪除一個項目
            this.broker.cacher.del("users.list");
            // 刪除多個項目
            this.broker.cacher.del([ "users.model:5", "users.model:8" ]);
        }
    }
}
如果要清除多個服務實例之間的快取項目,推薦的做法是使用廣播事件。注意,此方法僅適用於非集中管理的快取類型,如 Memory 或 MemoryLRU 。
範例:
module.exports = {
    name: "users",
    actions: {
        create(ctx) {
            // 建立新的使用者實體
            const user = new User(ctx.params);
            // 清除快取方法
            this.cleanCache();
            return user;
        }
    },
    methods: {
        cleanCache() {
            // 發送廣播事件,包含自己的所有的服務實例都會收到事件
            this.broker.broadcast("cache.clean.users");
        }
    },
    events: {
        "cache.clean.users"() {
            if (this.broker.cacher) {
                this.broker.cacher.clean("users.**");
            }
        }
    }
};
服務相依是很常見的狀況。例如 posts 服務儲存了一些來自 users 服務的快取項目。
範例:
{
    _id: 1,
    title: "My post",
    content: "Some content",
    author: {
        _id: 130,
        fullName: "John Doe",
        avatar: "https://..."
    },
    createdAt: 1519729167666
}
由於範例中的 author 儲存了一些來自 users 服務的資訊,所以當 users 服務清除了快取項目,連帶 posts 也應該要清除自己的快取項目。這種情況下,你應該也要在 posts 服務中訂閱 cache.clear.users 廣播事件來清除快取。
但有一個更簡單的方法,你可以建立一個 CacheCleaner 的混合函數,並在依賴的服務中加入。
cache.cleaner.mixin.js
module.exports = function (serviceNames) {
    const events = {};
    serviceNames.forEach(name => {
        events[`cache.clean.${name}`] = function () {
            if (this.broker.cacher) {
                this.logger.debug(`Clear local '${this.name}' cache`);
                this.broker.cacher.clean(`${this.name}.*`);
            }
        };
    });
    return {
        events
    };
};
posts.service.js
const CacheCleaner = require("./cache.cleaner.mixin");
module.exports = {
    name: "posts",
    mixins: [CacheCleaner([
        "users",
        "posts"
    ])],
    actions: {
        //...
    }
};
Moleculer 還支援快取鎖定功能。詳情請參考 Add cache lock[2] 這個 PR。
範例:啟用鎖定
const broker = new ServiceBroker({
    cacher: {
        ttl: 60,
        lock: true, // 啟用鎖定,預設為關閉。
    }
});
範例:帶有存活時間的鎖定
const broker = new ServiceBroker({
    cacher: {
        ttl: 60,
        lock: {
            ttl: 15, // 鎖定最大的存活時間 (秒)
            staleTime: 10, // 如果存活時間小於此時間,表示資源過期了
        }
    }
});
範例:禁用鎖定
const broker = new ServiceBroker({
    cacher: {
        ttl: 60,
        lock: {
            enable: false, // 關閉
            ttl: 15, // 鎖定最大的存活時間 (秒)
            staleTime: 10, // 如果存活時間小於此時間,表示資源過期了
        }
    }
});
範例:Redis 快取帶有 redlock 函式庫
const broker = new ServiceBroker({
  cacher: {
    type: "Redis",
    options: {
      // 鍵的前綴
      prefix: "MOL",
      // 存活時間
      ttl: 30,
      // Redis 客戶端監控
      monitor: false,
      // Redis 設定
      redis: {
        host: "redis-server",
        port: 6379,
        password: "1234",
        db: 0
      },
      lock: {
        ttl: 15, // 鎖定最大的存活時間 (秒)
        staleTime: 10, // 如果存活時間小於此時間,表示資源過期了
      },
      // Redlock 設定
      redlock: {
        // Redis 客戶端。支援 node-redis 或 ioredis 。 預設只用 local 客戶端
        clients: [client1, client2, client3],
        // 預期時間浮動 (毫秒),請參閱:
        // https://redis.io/docs/reference/patterns/distributed-locks/
        driftFactor: 0.01,
        // 在拋出錯誤前,嘗試鎖定資源的最大次數
        retryCount: 10,
        // 嘗試時的等待時間 (毫秒)
        retryDelay: 200,
        // 最大隨機時間,此時間會加到嘗試時間,以提升高度搶奪下的效率 (毫秒),請參閱:
        // https://aws.amazon.com/tw/blogs/architecture/exponential-backoff-and-jitter/
        retryJitter: 200
      }
    }
  }
});
Memory 快取是一個內建的記憶體快取模組,它會將快取項目儲存在記憶體中。
範例:快速使用,可以設為 "Memory" 或 true
const broker = new ServiceBroker({
    cacher: "Memory"
});
範例:選項設定方式
| 名稱 | 類型 | 預設值 | 說明 | 
|---|---|---|---|
ttl | 
<Number> | null | 
存活時間 (秒) | 
clone | 
<Boolean> | <Function> | false | 
深度複製 | 
keygen | 
<Function> | null | 
客製化鍵值產生器 | 
maxParamsLength | 
<Number> | null | 
最大快取鍵長度 | 
lock | 
<Boolean> | <Object> | null | 
啟用快取鎖定 | 
const broker = new ServiceBroker({
    cacher: {
        type: "Memory",
        options: {
            ttl: 30 // 設定存活時間,若要關閉請設為 0 或 null 。
            clone: true // 深拷貝
        }
    }
});
範例:客製化深度複製函數
快取會使用 Lodash 的 _.cloneDeep 方法來深度複製,如果不想使用,可將 clone 選項設為一個客製化函數來取代。
const broker = new ServiceBroker({
    cacher: {
        type: "Memory",
        options: {
            clone: data => JSON.parse(JSON.stringify(data))
        }
    }
});
MemoryLRU 是一個內建的 LRU 快取模組。它會刪除最近最少用的項目。
使用前請安裝 lru-cache[3] 套件
npm install lru-cache --save。
範例:快速使用
const broker = new ServiceBroker({
    cacher: "MemoryLRU"
});
範例:選項設定方式
| 名稱 | 類型 | 預設值 | 說明 | 
|---|---|---|---|
ttl | 
<Number> | null | 
存活時間 (秒) | 
max | 
<Number> | null | 
快取中最大的項目數量 | 
clone | 
<Boolean> | <Function> | false | 
深度複製 | 
keygen | 
<Function> | null | 
客製化鍵值產生器 | 
maxParamsLength | 
<Number> | null | 
最大快取鍵長度 | 
lock | 
<Boolean> | <Object> | null | 
啟用快取鎖定 | 
Redis 快取是一個內建基於 Redis 分散式的快取模組。如果你有多個服務實例,當一個服務實例儲存了一些快取,其它的實例也能夠透過 Redis 找到它。
使用前請安裝 ioredis[4] 套件
npm install ioredis --save。
範例:快速使用,預設連線為 redis://localhost:6379
const broker = new ServiceBroker({
    cacher: "Redis"
});
範例:連線到 Redis 服務器
const broker = new ServiceBroker({
    cacher: "redis://redis-server:6379"
});
範例:選項設定方式
| 名稱 | 類型 | 預設值 | 說明 | 
|---|---|---|---|
prefix | 
<String> | null | 
鍵的前綴。 | 
ttl | 
<Number> | null | 
存活時間 (秒)。 | 
monitor | 
<Boolean> | false | 
Redis 客戶端監控[5] 。 | 
redis | 
<Object> | null | 
客製化 Redis 選項[6] 。 | 
keygen | 
<Function> | null | 
客製化鍵值產生器。 | 
maxParamsLength | 
<Number> | null | 
最大快取鍵長度。 | 
serializer | 
<String> | "JSON" | 
內建序列化器。 | 
cluster | 
<Object> | null | 
Redis 客戶端叢集設定[7] 。 | 
lock | 
<Boolean> | <Object> | null | 
啟用快取鎖定。 | 
pingInterval | 
<Number> | null | 
每毫秒發出 Redis PING 命令。用於使可能閒置逾時的連線保持活躍狀態。 | 
const broker = new ServiceBroker({
    cacher: {
        type: "Redis",
        options: {
            // 鍵的前綴
            prefix: "MOL",            
            // 存活時間
            ttl: 30, 
            // Redis 客戶端監控
            monitor: false 
            // Redis 設定
            redis: {
                host: "redis-server",
                port: 6379,
                password: "1234",
                db: 0
            }
        }
    }
});
範例:使用 MessagePack 序列化器
你可以設定一個序列化器給 Redis 快取使用,它預設會使用 JSON 序列化器。
const broker = new ServiceBroker({
    nodeID: "node-123",
    cacher: {
        type: "Redis",
        options: {
            ttl: 30,
            // 使用 MessagePack 序列化器儲存資料
            serializer: "MsgPack",
            redis: {
                host: "my-redis"
            }
        }
    }
});
範例:使用 Redis 客戶端叢集
const broker = new ServiceBroker({
    cacher: {
        type: "Redis",
        options: {
            ttl: 30, 
            cluster: {
                nodes: [
                    { port: 6380, host: "127.0.0.1" },
                    { port: 6381, host: "127.0.0.1" },
                    { port: 6382, host: "127.0.0.1" }
                ],
                options: {
					// ...
				}
            }   
        }
    }
});
你也可以建立客製化的快取模組,官方建議可以參考 記憶體快取[8] 或 Redis 快取[9] 的原始碼來修改,再實作 get 、 set 、 del 、 clean 方法。
範例:建立客製化快取
my-cacher.js
const BaseCacher = require("moleculer").Cachers.Base;
class MyCacher extends BaseCacher {
    async get(key) { /*...*/ }
    async set(key, data, ttl) { /*...*/ }
    async del(key) { /*...*/ }
    async clean(match = "**") { /*...*/ }
}
module.exports = MyCacher;
範例:使用客製化快取
const { ServiceBroker } = require("moleculer");
const MyCacher = require("./my-cacher");
const broker = new ServiceBroker({
    cacher: new MyCacher()
});
[1] Caching, https://moleculer.services/docs/0.14/caching.html
[2] Add cache lock, https://github.com/moleculerjs/moleculer/pull/490
[3] lru-cache, https://github.com/isaacs/node-lru-cache
[4] ioredis, https://github.com/luin/ioredis
[5] ioredis Monitor, https://github.com/luin/ioredis#monitor
[6] ioredis Connect to Redis, https://github.com/luin/ioredis#connect-to-redis
[7] ioredis Cluster, https://github.com/luin/ioredis#cluster
[8] Moleculer Memory Cacher, https://github.com/moleculerjs/moleculer/blob/master/src/cachers/memory.js
[9] Moleculer Redis Cacher, https://github.com/moleculerjs/moleculer/blob/master/src/cachers/redis.js